package com.hyt.it.ogt.ks.controller;

import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.hyt.common.apiInfor.ApiResponse;
import com.hyt.common.apiInfor.DataType;
import com.hyt.common.apiInfor.ParamType;
import com.hyt.core.util.UUIDUtils;
import com.hyt.it.ogt.cj.model.entity.ProjectImageStandard;
import com.hyt.it.ogt.cj.model.vo.ProjectCollectInfoVo;
import com.hyt.it.ogt.ks.base.BaseController;
import com.hyt.it.ogt.ks.config.ConfigManager;
import com.hyt.it.ogt.ks.config.KsBizRunTimeException;
import com.hyt.it.ogt.ks.enums.*;
import com.hyt.it.ogt.ks.enums.log.ApplyCheckState;
import com.hyt.it.ogt.ks.enums.log.CandidateOperatorLogType;
import com.hyt.it.ogt.ks.enums.log.CandidateWarnLogType;
import com.hyt.it.ogt.ks.enums.log.LogEventType;
import com.hyt.it.ogt.ks.feign.FileClient;
import com.hyt.it.ogt.ks.feign.KwClient;
import com.hyt.it.ogt.ks.feign.model.*;
import com.hyt.it.ogt.ks.model.dto.UpdateCandidateDTO;
import com.hyt.it.ogt.ks.model.entity.ApplyCheck;
import com.hyt.it.ogt.ks.model.entity.CandidateLogOther;
import com.hyt.it.ogt.ks.model.entity.CandidateProcess;
import com.hyt.it.ogt.ks.model.vo.*;
import com.hyt.it.ogt.ks.model.vo.log.AppVo;
import com.hyt.it.ogt.ks.service.*;
import com.hyt.it.ogt.ks.util.EnCacheUtil;
import com.hyt.it.ogt.ks.util.KeyDefineCommon;
import io.swagger.annotations.*;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.MediaType;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.validation.Valid;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.util.HashMap;
import java.util.List;
import java.util.Optional;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

/**
 *
 *
 * @author linjd
 * @since 2020/6/10 15:44
 */
@RestController
@RequestMapping("ks/infor/")
@Api(tags = "2.考试信息接口", value = "考试配置信息")
@ApiSort(2)
@Slf4j
public class ExamInfoController extends BaseController {
    @Resource
    private KwClient kwClient;
    @Resource
    private FileClient fileClient;
    @Resource
    private ICandidateProcessService iCandidateProcessService;
    @Resource
    private IExamCacheService iExamCacheService;
    @Resource 
    private ConfigManager configManager;
    @Resource
    private IAsyncService iAsyncService;
    @Resource
    private EnCacheUtil enCacheUtil;
    @Resource
    private ExamInfoCacheService examInfoCacheService;
    @Resource
    private RedisTemplate redisTemplate;
    @Resource
    private ILogService iLogService;
    @Resource
    private ICandidateLogOtherService iCandidateLogOtherService;
    @Resource
    private IApplyCheckService iApplyCheckService;

    @GetMapping("/getExamInfo")
    @ApiOperation(value = "2.1.1  获取考试信息-登录页面", notes = "状态码：20000:成功，18313：获取考试信息失败，18314：异常")
    @ApiOperationSort(1)
    public ApiResponse<ExamDTO> getExamInfo(String examId) {
        try {
            ExamDTO examDTO = examInfoCacheService.getExamInfoFromCache(examId);
            Exam exam = examDTO.getExam();
            if (StringUtils.isNotBlank(exam.getTaskName())) {
            	exam.setExamName(exam.getTaskName());
            }
            examDTO.getExam().setAgreement(null);
            exam.setOfficeName(iExamCacheService.getProjectName(exam.getOfficeId()));
            SkinVO skinVo = Optional.ofNullable(iExamCacheService.getProjectSkin(exam.getOfficeId())).orElse(new SkinVO());
            if(StringUtils.isNotBlank(skinVo.getBackground())) {
                if(skinVo.getBackground().startsWith("http")) {
                    exam.setBackground(skinVo.getBackground());
                } else {
                    exam.setBackground(configManager.getNginxPath() + skinVo.getBackground());
                }
            }
            if(StringUtils.isNotBlank(skinVo.getLogo())) {
                if(skinVo.getLogo().startsWith("http")) {
                    exam.setLogo(skinVo.getLogo());
                } else {
                    exam.setLogo(configManager.getNginxPath() + skinVo.getLogo());
                }
            }
            List<ExamConfigVO> list = iExamCacheService.getExamConfigCache(examId);
            list = list.stream().filter(
                    l -> l.getConfigCode().equals(ExamConfigCode.EXAM_CONFIG_VIDEO_DEVICE_CHECK.getCode())
            ).collect(Collectors.toList());
            Boolean videoDeviceCheck = list != null && list.size() == 1 ? list.get(0).getConfigValue().equals("true") : false;
            examDTO.setVideoDeviceCheck(videoDeviceCheck);
            examDTO.setWebsocket(configManager.getWebsocketUrl());
            return ApiResponse.<ExamDTO>builder().code(BizStatusCode.KS_SUCCESS_OK.getCode()).data(examDTO).build();
        } catch (Exception e) {
            log.error("获取考试信息-登录页面异常码{}：{}", BizStatusCode.OPEN_LOGIN_PAGE_FAIL.getCode(), e);
            return ApiResponse.<ExamDTO>builder().code(BizStatusCode.OPEN_LOGIN_PAGE_FAIL.getCode()).build();
        }
    }

    @PostMapping("/doLogin")
    @ApiOperation(value = "2.1.2 考生登陆", notes = "状态码：20000成功，18014:考试项目未发布,18016：异常")
    @ApiOperationSort(2)
    public ApiResponse<CandidateInfoVO> doLogin(@RequestBody @Valid CandidateLoginRequestVO param, HttpServletRequest request, BindingResult bindingResult) {
        log.info("doLogin接口参数:{}、{}、{}", JSON.toJSONString(param));
        ApiResponse<CandidateInfoVO> api = new ApiResponse<>();
        if(bindingResult.hasErrors()) {
            return ApiResponse.<CandidateInfoVO>builder().code(BizStatusCode.KW_INVALID_PARAMETER.getCode()).build();
        }
        param.setAdmissionNumber(param.getAdmissionNumber().trim());
        String lockKey = KeyDefineCommon.getExamKsDoLoginKey(param.getExamId(), param.getAdmissionNumber());
        try {
            if(!redisTemplate.opsForValue().setIfAbsent(lockKey,"1")){
                log.error("考生登录考试，重复提交，考生准备证号：【{}】,考试id:【{}】", param.getAdmissionNumber(), param.getAdmissionNumber());
                return ApiResponse.<CandidateInfoVO>builder().code(BizStatusCode.DO_LOGIN_REPEAT_FAIL.getCode()).build();
            }
            api = iCandidateProcessService.doLogin(param, request);
            return api;
        } catch (KsBizRunTimeException ex) {
            return ApiResponse.<CandidateInfoVO>builder().code(ex.getCode()).build();
        } catch (Exception ex) {
            logger.error("通过考生证考证获取考试信息失败:{}", ex);
            return ApiResponse.<CandidateInfoVO>builder().code(BizStatusCode.DO_LOGIN_FAIL.getCode()).build();
        } finally {
            redisTemplate.delete(lockKey);
            //2.4.p4加的，一个单题限制时长，在清理进度时会有一个redis 的锁，如果这个时候考生重新登录了，不会因为收到websocket消息触发清理锁，这里加一个清理，
            //就算没有这个锁，也不影响正常功能
            String key = KeyDefineCommon.getCandidateResatTime(api.getData().getExamId(), api.getData().getCandidateId());
            redisTemplate.delete(key);
        }
    }    
    
    @ApiOperation(value = "2.1.3 获取考试须知", notes = "获取考试须知文本内容，状态码：20000:成功，18007：异常", response = ApiResponse.class)
    @ApiImplicitParams({@ApiImplicitParam(name = "examId", value = "考试项目id", dataType = DataType.STRING, paramType = ParamType.QUERY, required = true)})
    @ApiOperationSort(3)
    @GetMapping("/getAgreement")
    public Object getAgreement(String examId) {
        if (StringUtils.isEmpty(examId)) {
            return ApiResponse.<ExamAgreement>builder().code(BizStatusCode.KW_INVALID_PARAMETER.getCode()).build();
        }
        try {
            String repRtn = kwClient.getAgreement(examId);
            ApiResponse<ExamAgreement> clientResponse = ApiResponse.ConvertRet(repRtn, ExamAgreement.class);
            if (clientResponse.getCode().intValue() != BizStatusCode.KW_SUCCESS_OK.getCode().intValue()) {
                return clientResponse;
            }
            ExamAgreement examAgreement = clientResponse.getData();
            return ApiResponse.<ExamAgreement>builder().code(BizStatusCode.KS_SUCCESS_OK.getCode()).data(examAgreement).build();
        } catch (Exception ex) {
            log.error(StringUtils.join(BizStatusCode.GET_CANDIDATE_KNOWN.getCode(), ": "), ex);
            return ApiResponse.<ExamAgreement>builder().code(BizStatusCode.GET_CANDIDATE_KNOWN.getCode()).build();
        }
    }

    @ApiOperation(value = "2.1.4 更新考生信息", notes = "更新考生信息，状态码：20000:成功，18013：异常", response = ApiResponse.class)
    @ApiOperationSort(4)
    @PostMapping("/updateCandidateInfo")
    public ApiResponse updateCandidateInfo(@RequestBody CandidateInfoConfigVO candidateInfo) {
        try {
        	CandidateProcess candidateProcess = iCandidateProcessService.lambdaQuery()
                    .select(CandidateProcess::getExamStatus)
                    .eq(CandidateProcess::getExamId, candidateInfo.getExamId())
                    .eq(CandidateProcess::getCandidateId, candidateInfo.getCandidateId())
                    .eq(CandidateProcess::getDelFlag, false).one();
        	if (candidateProcess != null &&
                    candidateProcess.getExamStatus().intValue() >= CandidateStatusCode.COMMIT.getCode().intValue()) {
        		 return ApiResponse.builder().code(BizStatusCode.UPDATE_CANDIDATE_FAIL.getCode()).build();
        	}
            UpdateCandidateDTO dto = new UpdateCandidateDTO();
        	dto.setExamId(candidateInfo.getExamId());
        	dto.setCandidateId(candidateInfo.getCandidateId());
        	dto.setInfoData(JSON.toJSONString(candidateInfo.getCandidateInfo()));
            // 更新考务考生信息
            String repRtn = kwClient.updateCandidate(JSON.toJSONString(dto));
            ApiResponse<String> clientResponse = ApiResponse.ConvertRet(repRtn, String.class);
            if (clientResponse.getCode().intValue() != BizStatusCode.KW_SUCCESS_OK.getCode().intValue()) {
                return clientResponse;
            }

            iLogService.sureInfoLog(candidateInfo.getExamId(), candidateInfo.getCandidateId());
        } catch (Exception ex) {
            logger.error("上传文件报错！", ex);
            return ApiResponse.builder().code(BizStatusCode.SAVE_ENTITY_FAIL.getCode()).build();
        }
        return ApiResponse.builder().code(BizStatusCode.KS_SUCCESS_OK.getCode()).build();
    }

    @ApiOperation(value = "2.1.5 文件上传", notes = "状态码：20000:成功，18011：上传图片格式不正确，18308：异常", response = ApiResponse.class)
    @ApiImplicitParams({@ApiImplicitParam(name = "fileType", value = "上传的文件类型: 1:表示上传的格式为图片，2:表示上传的格式为zip或者rar，null或者0表示不校验上传文件格式", dataType = DataType.INT, paramType = ParamType.QUERY, required = false)})
    @ApiOperationSort(5)
    @PostMapping("/uploadFile")
    public Object uploadFile(@RequestParam MultipartFile file, @RequestParam(required = false) Integer fileType){
        try{
            if(fileType != null){
                if(fileType == 1){
                    ApiResponse rtn = validImageFile(file);
                    if (rtn.getCode() != BizStatusCode.KS_SUCCESS_OK.getCode().intValue()) {
                        return rtn;
                    }
                }else if(fileType == 2) {
                    ApiResponse rtn = validPackageFile(file);
                    if (rtn.getCode() != BizStatusCode.KS_SUCCESS_OK.getCode().intValue()) {
                        return rtn;
                    }
                }
            }
            String fileSysPath = fileClient.uploadFile(file);
            return ApiResponse.builder().code(BizStatusCode.KS_SUCCESS_OK.getCode()).data(fileSysPath).build();
        } catch (Exception ex) {
            logger.error("上传文件报错！", ex);
            return ApiResponse.builder().code(BizStatusCode.UPLOAD_FILE_FAIL.getCode()).build();
        }
    }

    public ApiResponse validImageFile(MultipartFile pictureFile) {
        String fileType = FilenameUtils.getExtension(pictureFile.getOriginalFilename());
        String reg = "(?i)^(jpeg|jpg|gif|bmp|png)$";
        // 格式不正确
        if (!Pattern.compile(reg).matcher(fileType.toLowerCase()).find()) {
            String msg = "上传图片格式不正确，请上传（.jpg/.gif/.bmp/.png）格式";
            return ApiResponse.builder().code(BizStatusCode.UPLOAD_IMAGES_FORMAT_FAIL.getCode()).data(msg).build();

        }
        return ApiResponse.builder().code(BizStatusCode.KS_SUCCESS_OK.getCode()).build();
    }

    public ApiResponse validPackageFile(MultipartFile pictureFile) {
        String fileType = FilenameUtils.getExtension(pictureFile.getOriginalFilename());
        String reg = "(?i)^(zip|rar)$";
        // 格式不正确
        if (!Pattern.compile(reg).matcher(fileType.toLowerCase()).find()) {
            String msg = "上传文件格式不正确，请上传（zip或者rar）格式";
            return ApiResponse.builder().code(BizStatusCode.UPLOAD_IMAGES_FORMAT_FAIL.getCode()).data(msg).build();

        }
        return ApiResponse.builder().code(BizStatusCode.KS_SUCCESS_OK.getCode()).build();
    }

    @ApiOperation(value = "2.1.6 获取考试配置信息", notes = "状态码：20000:成功，18303：异常", response = ExamConfigVO.class)
    @ApiOperationSort(6)
    @GetMapping("/getExamInfoConfig")
    public ApiResponse getExamInfoConfig(String examId) {
        try {
            List<ExamConfigVO> list = iExamCacheService.getExamConfigCache(examId);
            return ApiResponse.builder().code(BizStatusCode.KS_SUCCESS_OK.getCode()).data(list).build();
        } catch (Exception e) {
            log.error("获取考试信息-登录页面异常码："+BizStatusCode.GET_EXAM_CONFIG_FAIL.getCode()+"，", e);
            return ApiResponse.builder().code(BizStatusCode.GET_EXAM_CONFIG_FAIL.getCode()).build();
        }
    }

    @ApiOperation(value = "2.1.7 获取考生状态", notes = "返回值state状态参考登录接口，状态码：20000:成功，18318：不存在考生，42020111：异常", response = ApiResponse.class)
    @ApiImplicitParams({
            @ApiImplicitParam(name = "examId", value = "考试项目id", dataType = DataType.STRING, paramType = ParamType.QUERY, required = true),
            @ApiImplicitParam(name = "candidateId", value = "考生id", dataType = DataType.STRING, paramType = ParamType.QUERY, required = true)
    })
    @ApiOperationSort(7)
    @GetMapping("getCandidateState")
    public Object getCandidateState(String examId, String candidateId){
        try {
            String json = kwClient.checkExamStartTime(examId);
            ApiResponse response = JSON.parseObject(json, ApiResponse.class);
            if(response.getData() != null && !Boolean.valueOf(response.getData().toString()).booleanValue()) {
                return ApiResponse.builder().code(BizStatusCode.EXAM_NOT_START.getCode()).build();
            }
            QueryWrapper<CandidateProcess> query = Wrappers.query();
            query.lambda().select(CandidateProcess::getExamStatus, CandidateProcess::getLoginCount)
                    .eq(CandidateProcess::getExamId, examId)
                    .eq(CandidateProcess::getCandidateId, candidateId);
            CandidateProcess candidateProcess = iCandidateProcessService.getOne(query);
            if(candidateProcess != null){
                HashMap<String, Object> map = new HashMap<>();
                map.put("state", candidateProcess.getExamStatus());
                return ApiResponse.builder()
                        .code(BizStatusCode.KS_SUCCESS_OK.getCode())
                        .data(map)
                        .build();
            }else{
                return ApiResponse.builder().code(BizStatusCode.NOT_EXIST_CANDIDATE.getCode()).build();
            }
        } catch (Exception e) {
            logger.error(StringUtils.join(BizStatusCode.GET_CANDIDATE_STATE_FAIL.getCode(), ": "), e);
            return ApiResponse.builder().code(BizStatusCode.GET_CANDIDATE_STATE_FAIL.getCode()).build();
        }
    }
    
    @ApiOperation(value = "2.1.8 获取考生注册信息", notes = "20000:成功，42040102：异常", response = ApiResponse.class)
    @ApiImplicitParams({@ApiImplicitParam(name = "examId", value = "考试项目id", dataType = DataType.STRING, paramType = ParamType.QUERY, required = true) })
    @ApiOperationSort(8)
   	@GetMapping("getRegisterInfo")
    public Object getRegisterInfo(String examId) {
    	try {
	    	log.info("获取考生注册信息请求参数内容：【{}】", examId.toString());		    	
	        return ApiResponse.builder()
	        		.code(BizStatusCode.KS_SUCCESS_OK.getCode())
	        		.data(kwClient.queryByProjectId(examId))
	        		.build();	              
	    }catch (Exception e) {
	        log.error(StringUtils.join(BizStatusCode.GET_REGISTER_INFO_ERROR.getCode(), ": "), e);
	        return ApiResponse.builder()
	                .code(BizStatusCode.GET_REGISTER_INFO_ERROR.getCode())
	                .build();
	    }		
    }
    
    @ApiOperation(value = "2.1.9 考生注册", notes = "20000:成功，42020901：参数校验错误,18320: 手机+邮箱已存在, 42020902：异常", response = ApiResponse.class)
	@ApiOperationSort(9)
	@PostMapping("/registerStudent")
    public Object registerStudent(@RequestBody @Valid ExamCandidateInfoRegisterVO examCandidateInfoRegisterVO, BindingResult bindingResult) {
    	try {
	    	log.info("考生注册请求参数内容：【{}】", examCandidateInfoRegisterVO.toString());
	        if (bindingResult.hasErrors()) {
	            log.info(StringUtils.join(BizStatusCode.REGISTER_INVALID_PARAMETER.getCode(), ":", bindingResult.getAllErrors()));
	            return ApiResponse.builder()
	                    .code(BizStatusCode.REGISTER_INVALID_PARAMETER.getCode())
	                    .data(bindingResult.getAllErrors())
	                    .build();
	        }
	        String repRtn = kwClient.registerStudent(JSON.toJSONString(examCandidateInfoRegisterVO));
	        ApiResponse<String> clientResponse = ApiResponse.ConvertRet(repRtn, String.class);
	        if(clientResponse.getCode().intValue() != BizStatusCode.KS_SUCCESS_OK.getCode().intValue()) {
	        	return clientResponse;
	        }else {
	        	return ApiResponse.builder().code(BizStatusCode.KS_SUCCESS_OK.getCode()).data(clientResponse.getData()).build();
	        }	        
	    }catch (Exception e) {
	        log.error(StringUtils.join(BizStatusCode.REGISTER_ERROR.getCode(), ": "), e);
	        return ApiResponse.builder()
	                .code(BizStatusCode.REGISTER_ERROR.getCode())
	                .build();
	    }		
    }

    @GetMapping("/getCandidateInfo")
    @ApiOperation(value = "2.1.10 获取考生信息", notes = "状态码：20000:成功，18016：异常")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "examId", value = "考试项目id", dataType = DataType.STRING, paramType = ParamType.QUERY, required = true),
            @ApiImplicitParam(name = "candidateId", value = "考生id", dataType = DataType.STRING, paramType = ParamType.QUERY, required = true)
    })
    @ApiOperationSort(10)
    public ApiResponse<CandidateDataVO> getCandidateInfo(@RequestParam String examId, @RequestParam String candidateId) {
        try {
            LambdaQueryWrapper<CandidateProcess> query = Wrappers.lambdaQuery();
            query.select(CandidateProcess::getAdmissionNumber, CandidateProcess::getCheckedIdCard,
                            CandidateProcess::getTakePicture)
                    .eq(CandidateProcess::getExamId, examId)
                    .eq(CandidateProcess::getCandidateId, candidateId)
                    .eq(CandidateProcess::getDelFlag, false);
            CandidateProcess candidateProcess = iCandidateProcessService.getOne(query);

            // 获取考生信息内容
            String repRtn = kwClient.getExamCandidateInfo(examId, candidateProcess.getAdmissionNumber());
            ApiResponse<ExamCandidate> clientResponse = ApiResponse.ConvertRet(repRtn, ExamCandidate.class);
            // 判断考务是否有考生
            if (clientResponse.getCode().intValue() != BizStatusCode.KW_SUCCESS_OK.getCode().intValue()) {
                return ApiResponse.<CandidateDataVO>builder().code(clientResponse.getCode()).build();
            }
            ExamCandidate examCandidate = clientResponse.getData();
            List<ExamCandidateInfo> examCandidateInfos = examCandidate.getExamCandidateInfos();
            List<ProjectCollectInfoVo> configList = kwClient.queryByProjectId(examId);
            
            Optional<ProjectCollectInfoVo> projectCollectPhotoInfo = configList.stream().filter(cl -> 
            		cl.getComponentName().equals(CollectInfoComponentNameCode.CERTIFICATE_PHOTO.getCode())).findFirst();
            if(projectCollectPhotoInfo.isPresent()) {
            	Optional<ExamCandidateInfo> examCandidatePhotoInfo = examCandidateInfos.stream().filter(eci -> 
            		eci.getConfigCode().equals(projectCollectPhotoInfo.get().getId())).findFirst();
            	if (examCandidatePhotoInfo.isPresent() && examCandidatePhotoInfo.get().getConfigValue() != null
            			&& !examCandidatePhotoInfo.get().getConfigValue().contains("/")
            			&& !examCandidatePhotoInfo.get().getConfigValue().equals(",")) {
            		examCandidatePhotoInfo.get().setConfigValue("");
            	}
            }
            List<ExamConfigVO> examConfigList = iExamCacheService.getExamConfigCache(examId);
            Optional<ExamConfigVO> examConfig = examConfigList.stream().filter(e -> e.getConfigCode().equals(ExamConfigCode.EXAM_CONFIG_CODE_APPLY_PASS_AUTHENTICATION.getCode())).findFirst();
            Integer idCardReviewState = null;
            if (examConfig.isPresent() && examConfig.get().getConfigValue().equals("true") && !candidateProcess.getCheckedIdCard()) {
                QueryWrapper<ApplyCheck> queryWrapper = new QueryWrapper<>();
                queryWrapper.lambda().eq(ApplyCheck::getExamId, examId)
                        .eq(ApplyCheck::getCandidateId,candidateId)
                        .eq(ApplyCheck::getEventType, LogEventType.OPERATOR_LOG.getCode())
                        .eq(ApplyCheck::getOperateType, CandidateOperatorLogType.ID_FACE_SKIP_APPLY.getCode())
                        .eq(ApplyCheck::getState, ApplyCheckState.WAIT_HANDLE.getCode())
                        .orderByDesc(ApplyCheck::getCreateDate)
                        .last("limit 1");
                ApplyCheck applyCheck = iApplyCheckService.getOne(queryWrapper);
                if (applyCheck != null && StringUtils.isNotBlank(applyCheck.getOperateContent())) {
                    idCardReviewState = applyCheck.getState();
                }
            }
            return ApiResponse.<CandidateDataVO>builder().code(BizStatusCode.KS_SUCCESS_OK.getCode())
                    .data(CandidateDataVO.builder().candidateInfo(examCandidateInfos).candidateConfig(configList)
                            .checkedIdCard(candidateProcess.getCheckedIdCard())
                            .takePicture(candidateProcess.getTakePicture())
                            .idCardReviewState(idCardReviewState).build()).build();
        } catch (Exception ex) {
            logger.error("获取考生信息失败", ex);
            return ApiResponse.<CandidateDataVO>builder().code(BizStatusCode.GET_CANDIDATE_INFO_ERROR.getCode()).build();
        }
    }
    
    @ApiOperation(value = "2.1.11 上传附件（上传MultipartFile）", notes = "返回文件路径:14001:成功 42021103:失败,")
	@ApiOperationSort(value = 11)
	@RequestMapping(value = "/uploadEnclosureFile",
		method = RequestMethod.POST, consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
	public Object uploadEnclosureFile(@RequestPart(value = "file") MultipartFile file, String projectCollectInfoId) throws IOException {
		try {
			//判断附件是否合格
			String string = vaildUploadEnclosureFile(file, projectCollectInfoId);
			if ("".equals(string)) {
				String pathString = fileClient.uploadFile(file);
				return ApiResponse.
						builder().
						code(BizStatusCode.KS_SUCCESS_OK.getCode()).
						data(pathString).build();
			} else {
				return ApiResponse.builder().code(BizStatusCode.ENCLOURSE_FILE_ERROR.getCode()).data(string).build();
			}
		} catch (Exception e) {
			return ApiResponse.builder().code(BizStatusCode.ENCLOURSE_FILE_ERROR.getCode()).data(e.toString()).build();
		}
	}
    
    private String vaildUploadEnclosureFile(MultipartFile multipartFile, String projectCollectInfoId) {
    	String res = kwClient.getImageStandard(projectCollectInfoId);
    	log.info("获取附件标准接口：【{}】", res.toString());
        ApiResponse<ProjectImageStandard> resResponse = ApiResponse.ConvertRet(res, ProjectImageStandard.class);
        if (resResponse.getCode().intValue() != BizStatusCode.KW_SUCCESS_OK.getCode().intValue()) {
            throw new UnsupportedOperationException();
        }
		ProjectImageStandard projectImageStandard = resResponse.getData();

		try {
			long size = multipartFile.getSize(); // kb
			double fileSize = (double) size / (1024.00 * 1024); // M
			if (fileSize > Double.valueOf(projectImageStandard.getMaxSize())
					|| fileSize < Double.valueOf(projectImageStandard.getMinSize())) { // 判断附件大小
				// 单位m
				return "附件不合适.范围:" + projectImageStandard.getMinSize() + "- " + projectImageStandard.getMaxSize();
			}
		} catch (Exception e) {
			e.printStackTrace();
			return e.toString();
		}

		return "";
	}

    @ApiOperation(value = "2.1.12 上传考生照片（上传MultipartFile）", notes = "返回文件路径:14001:成功 42021203:失败,")
	@ApiOperationSort(value = 12)
	@RequestMapping(value = "/uploadPhoto",
		method = RequestMethod.POST, consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
	public Object uploadFile(@RequestPart(value = "file") MultipartFile file, String projectCollectInfoId) {
		try {
			String string = vaildUploadFile(file, projectCollectInfoId);
			if ("".equals(string)) {
				String pathString = fileClient.uploadFile(file);
				return ApiResponse.
						builder().
						code(BizStatusCode.KS_SUCCESS_OK.getCode()).
						data(pathString).build();
			} else {
				return ApiResponse.builder().code(BizStatusCode.UPLOAD_FILE_ERROR.getCode()).data(string).build();
			}
		} catch (Exception e) {
			return ApiResponse.builder().code(BizStatusCode.UPLOAD_FILE_ERROR.getCode()).data(e.toString()).build();
		}
	}
        
  	private String vaildUploadFile(MultipartFile multipartFile, String projectCollectInfoId) {
  		String res = kwClient.getImageStandard(projectCollectInfoId);
    	log.info("获取附件标准接口：【{}】", res.toString());
        ApiResponse<ProjectImageStandard> resResponse = ApiResponse.ConvertRet(res, ProjectImageStandard.class);
        if (resResponse.getCode().intValue() != BizStatusCode.KW_SUCCESS_OK.getCode().intValue()) {
            throw new UnsupportedOperationException();
        }
		ProjectImageStandard projectImageStandard = resResponse.getData();


  		// MultipartFile multipartFile = multipartFile; 获取请求传过来的MultipartFile
  		String fileName = multipartFile.getOriginalFilename();
  		String prefix = "";
  		if (StringUtils.isNotBlank(fileName)) {
  			prefix = fileName.substring(fileName.lastIndexOf("."));
  		}
  		try {
  			long size = multipartFile.getSize(); // kb
  			double fileSize = (double) size / (1024.00 * 1024); // M
  			if (fileSize > Double.valueOf(projectImageStandard.getMaxSize())
  					|| fileSize < Double.valueOf(projectImageStandard.getMinSize())) { // 判断图片大小
  				// 单位m
  				return "图片尺寸不合适.范围:" + projectImageStandard.getMinSize() + "- " + projectImageStandard.getMaxSize();
  			}
  			File file = File.createTempFile(prefix, String.valueOf(System.currentTimeMillis())); // 创建临时文件
  			FileUtils.copyInputStreamToFile(multipartFile.getInputStream(), file);
  			BufferedImage bufferedImage = ImageIO.read(file); // 通过临时文件获取图片流
  			if (bufferedImage == null) {
  				// 证明上传的文件不是图片，获取图片流失败，不进行下面的操作
  				return "上传的文件不是图片,请重新上传！";
  			}
  			Integer width = bufferedImage.getWidth(); // 通过图片流获取图片宽度
  			if (projectImageStandard.getMinWidth() > width || width > projectImageStandard.getMaxWidth()) {
  				return "照片宽度过大或过小，请重新上传！";
  			}
  			Integer height = bufferedImage.getHeight(); // 通过图片流获取图片高度
  			if (projectImageStandard.getMinHeight() > height || height > projectImageStandard.getMaxHeight()) {
  				return "照片高度过大或过小，请重新上传！";
  			}
  			// 省略逻辑判断
  		} catch (Exception e) {
  			// 省略异常操作
            log.error("{}", e);
  		}
  		return "";
  	}
  	
  	@ApiOperation(value = "2.1.13 获取倒计时(毫秒)", notes = "获取倒计时(毫秒)，状态码：20000:成功，18211：异常", response = ApiResponse.class)
    @ApiImplicitParams({
            @ApiImplicitParam(name = "examId", value = "考试项目id", dataType = DataType.STRING,
                    paramType = ParamType.QUERY, required = true),
            @ApiImplicitParam(name = "candidateId", value = "考生id", dataType = DataType.STRING,
                    paramType = ParamType.QUERY, required = true)
    })
    @ApiOperationSort(13)
    @GetMapping("/getCountDown")
    public Object getCountDown(@RequestParam String examId, @RequestParam String candidateId) {
  		log.info(" 获取倒计时(毫秒)接口请求参数内容：【{}】、【{}】", examId, candidateId);
        if (StringUtils.isEmpty(examId)) {
            return ApiResponse.<ExamAgreement>builder().code(BizStatusCode.KW_INVALID_PARAMETER.getCode()).build();
        }
        try {
            ExamDTO examDTO = examInfoCacheService.getExamInfoFromCache(examId);
            // 毫秒
            Long timeLength = 0L;
            if(examDTO.getExam().getBeginDate().isAfter(LocalDateTime.now())) {
            	Long milliSecond = LocalDateTime.now().toInstant(ZoneOffset.of("+8")).toEpochMilli();
            	timeLength = examDTO.getExam().getBeginDate()
                        .toInstant(ZoneOffset.of("+8")).toEpochMilli() - milliSecond;
            }
            // 毫秒比较
            if(timeLength >= configManager.getPaperAllowInitTime() * 60 * 1000){
                iAsyncService.initPaper(examId, candidateId);
            }
            return ApiResponse.builder().code(BizStatusCode.KS_SUCCESS_OK.getCode()).data(timeLength).build();
        } catch (Exception ex) {
            log.error(StringUtils.join(BizStatusCode.GET_COUNT_DOWN_ERROR.getCode(), ": "), ex);
            return ApiResponse.<ExamAgreement>builder().code(BizStatusCode.GET_COUNT_DOWN_ERROR.getCode()).build();
        }
    }
  	
  	@ApiOperation(value = "2.1.14 考试是否发布", notes = "状态码：20000:成功，18014:考试未发布，42011401：异常", response = ApiResponse.class)
    @ApiImplicitParams({@ApiImplicitParam(name = "examId", value = "考试项目id", dataType = DataType.STRING, paramType = ParamType.QUERY, required = true)})
    @ApiOperationSort(14)
  	@GetMapping("/getPublishExam")
    public Object getPublishExam(String examId) {
  		log.info(" 获取 考试是否发布接口请求参数内容：【{}】", examId);
  		try {
	        ExamDTO examDTO = examInfoCacheService.getExamInfoFromCache(examId);
	        Exam exam = examDTO.getExam();
	        if(!exam.getPublishExam()) {
	        	return ApiResponse.builder().code(BizStatusCode.EXAM_NO_PUBLISH_FAIL.getCode()).build();
	        }
	        return ApiResponse.builder().code(BizStatusCode.KS_SUCCESS_OK.getCode()).data(true).build();
  		} catch (Exception ex) {
            log.error(StringUtils.join(BizStatusCode.GET_PUBLISH_EXAM_ERROR.getCode(), ": "), ex);
            return ApiResponse.<ExamAgreement>builder().code(BizStatusCode.GET_PUBLISH_EXAM_ERROR.getCode()).build();
        }  	
  	}

    @ApiOperation(value = "2.1.15 手动清理考试信息二级缓存", response = Boolean.class)
    @ApiImplicitParams({@ApiImplicitParam(name = "examId", value = "考试项目id", dataType = DataType.STRING, paramType = ParamType.QUERY, required = true)})
    @ApiOperationSort(15)
    @GetMapping("/clearExamInfoCache")
    public Boolean clearExamInfoCache(@RequestParam String examId) {
        log.info(" 手动清理二级缓存当中的考试配置信息，接收参数：【{}】", examId);
        try {
            String key = KeyDefineCommon.getExamInfoCacheInfoKey(examId);
            return enCacheUtil.removeExamInfoToCache(key);
        } catch (Exception e) {
            log.error("手动清理二级缓存当中的考试配置信息失败，接收参数：【{}】，异常信息为:【{}】", examId,e);
            return false;
        }
    }

    @ApiOperation(value = "2.1.16 获取屏幕快照压缩百分比", notes = "状态码：20000:成功", response = ApiResponse.class)
    @ApiOperationSort(16)
    @GetMapping("/getScreenShotCompressPercentage")
    public Object getScreenShotCompressPercentage() {
        return ApiResponse.builder()
                .code(BizStatusCode.KS_SUCCESS_OK.getCode())
                .data(configManager.getScreenShotCompressPercentage())
                .build();
    }

    @ApiOperation(value = "2.1.22 回写设备检测状态", notes = "状态码：20000:成功", response = ApiResponse.class)
    @ApiOperationSort(22)
    @PutMapping("/updateDeviceCheckState")
    public ApiResponse updateDeviceCheckState(@RequestBody DeviceCheckResultVO param) {
        log.info(" 回写设备检测状态，接收参数：【{}, {}】", JSON.toJSONString(param));
        return kwClient.updateDeviceCheckState(param);
    }

    @GetMapping("/unifiedLogin")
    @ApiOperation(value = "2.1.17 统一登录接口", notes = "状态码：20000成功，42011701：异常")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "loginCode", value = "登录码", dataType = DataType.STRING,
                    paramType = ParamType.QUERY, required = true),
            @ApiImplicitParam(name = "loginType", value = "登录类型1:身份证号 2：手机号 3：准考证号", dataType = DataType.STRING,
                    paramType = ParamType.QUERY, required = true)
    })
    @ApiOperationSort(17)
    public Object unifiedLogin(@RequestParam String loginCode, @RequestParam String loginType) {
        log.info("2.1.17 统一登录接口请求参数: {}, {}", loginCode, loginType);
        String lockKey = KeyDefineCommon.getUifiedLoginData(loginCode,loginType);
        try {
            if(!redisTemplate.opsForValue().setIfAbsent(lockKey,"1")){
                log.error("考生统一登录考试，重复提交，考生登录码：【{}】, 登类型:【{}】",loginCode,loginType);
                return ApiResponse.<CandidateInfoVO>builder().code(BizStatusCode.DO_LOGIN_REPEAT_FAIL.getCode()).build();
            }
            String res = kwClient.unifiedLogin(loginCode, loginType);
            return  res;
        } catch (Exception ex) {
            logger.error("统一登录失败:{}", ex);
            return ApiResponse.builder().code(BizStatusCode.DO_LOGIN_FAIL.getCode()).build();
        } finally {
            redisTemplate.delete(lockKey);
        }
    }

    @GetMapping("/examList")
    @ApiOperation(value = "2.1.18 统一登录后进入考试列表接口", notes = "状态码：20000成功，42021801：异常")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "loginType", value = "登录类型1:身份证号 2：手机号 3：准考证号", dataType = DataType.STRING,
                    paramType = ParamType.QUERY, required = true),
            @ApiImplicitParam(name = "admissionNumber", value = "准考证号", dataType = DataType.STRING,
                    paramType = ParamType.QUERY, required = true),
            @ApiImplicitParam(name = "name", value = "姓名", dataType = DataType.STRING,
                    paramType = ParamType.QUERY, required = true),
            @ApiImplicitParam(name = "idCard", value = "身份证号", dataType = DataType.STRING,
                    paramType = ParamType.QUERY, required = true),
            @ApiImplicitParam(name = "phone", value = "手机号", dataType = DataType.STRING,
                    paramType = ParamType.QUERY, required = true)
    })
    @ApiOperationSort(18)
    public Object examList(@RequestParam String loginType, @RequestParam(required=false) String admissionNumber, @RequestParam String name, @RequestParam(required = false) String idCard, @RequestParam(required = false) String phone) {

        try {
            String res = kwClient.getUnifiedLoginExamInfoList(loginType, admissionNumber, name, idCard, phone);
            return  res;
        } catch (Exception ex) {
            logger.error("统一登录后进入考试列表信息失败:{}", ex);
            return ApiResponse.<CandidateExamInfosVO>builder().code(BizStatusCode.EXAM_LIST_FAILD.getCode()).build();
        }
    }

    @GetMapping("/officeInfo")
    @ApiOperation(value = "2.1.19 机构地址获取机构信息", notes = "")
    @ApiImplicitParams({
            @ApiImplicitParam(name = "officeShortName", value = "机构短名称", dataType = DataType.STRING,
                    paramType = ParamType.QUERY, required = true)
    })
    @ApiOperationSort(19)
    public ApiResponse<CandidateExamInfosVO> officeInfo(@RequestParam String officeShortName) {

        try {
            return null;//todo
        } catch (KsBizRunTimeException ex) {
            return ApiResponse.<CandidateExamInfosVO>builder().code(ex.getCode()).build();
        } catch (Exception ex) {
            logger.error("通过考生证考证获取考试信息失败:{}", ex);
            return ApiResponse.<CandidateExamInfosVO>builder().code(BizStatusCode.DO_LOGIN_FAIL.getCode()).build();
        } finally {
            //redisTemplate.delete(lockKey);
        }
    }

    @GetMapping("/getExamBaseInfo")
    @ApiOperation(value = "2.1.20  获取考试基本信息", notes = "状态码：20000:成功")
    @ApiOperationSort(20)
    public ApiResponse<ExamBaseInfoDTO> getExamBaseInfo(String examId) {

        log.info("2.1.20  获取考试基本信息参数：任务id：{}，试卷id：{}，版本号：{}",examId);

        try {
            ExamBaseInfoDTO examBaseInfoDTO = new ExamBaseInfoDTO();

            ExamDTO examDTO = examInfoCacheService.getExamInfoFromCache(examId);
            Exam exam = examDTO.getExam();
            //1.机构名称
            examBaseInfoDTO.setOfficeName(iExamCacheService.getProjectName(exam.getOfficeId()));
            //2.考试id
            examBaseInfoDTO.setRandomId(String.valueOf(exam.getRandomId()));

            List<ExamConfigVO> examConfigVOList = iExamCacheService.getExamConfigCache(examId);

            examConfigVOList.stream().forEach(examConfigVO -> {
                if (ExamConfigCode.EXAM_CONFIG_EARLY_LOG_ON_MINS.getCode().equals(examConfigVO.getConfigCode())) {
                    //3.提前登录时间
                    examBaseInfoDTO.setEarlyLogOnMins(examConfigVO.getConfigValue());
                }

                if (ExamConfigCode.EXAM_CONFIG_DELAY_LOG_ON_MINS.getCode().equals(examConfigVO.getConfigCode())) {
                    //4.迟到时间
                    examBaseInfoDTO.setDelayLogOnMins(examConfigVO.getConfigValue());
                }
            });


            //设置默认值
            if (StringUtils.isBlank(examBaseInfoDTO.getEarlyLogOnMins())) {
                examBaseInfoDTO.setEarlyLogOnMins("0");
            }
            if (StringUtils.isBlank(examBaseInfoDTO.getDelayLogOnMins())) {
                examBaseInfoDTO.setDelayLogOnMins("0");
            }

            //5.允许提前交卷时间，前端已经获取到了提前交卷时间，这里就不需要再通过试卷查询
          /*  PaperDataVO paperDataVO = examCacheService.getPaperInfoSimpleCache(paperId,paperVersion);
            examBaseInfoDTO.setHandInTime(paperDataVO.getHandInTime());*/

            return ApiResponse.<ExamBaseInfoDTO>builder()
                    .code(BizStatusCode.KS_SUCCESS_OK.getCode())
                    .data(examBaseInfoDTO)
                    .build();
        } catch (Exception e) {
            log.error("2.1.20  获取考试基本信息异常码{}：{}", BizStatusCode.EXAM_BASE_INFO_FAIL.getCode(), e);
            return ApiResponse.<ExamBaseInfoDTO>builder().code(BizStatusCode.EXAM_BASE_INFO_FAIL.getCode()).build();
        }
    }

    @ApiOperation(value = "2.1.21 写入手机操作轨迹", notes = "状态码：20000:成功", response = ApiResponse.class)
    @ApiOperationSort(21)
    @GetMapping("/saveAppLog")
    public Object saveAppLog(@RequestParam String examId, @RequestParam String candidateId, @RequestParam String operatorType, @RequestParam(required = false) String takeTime) {
        try {
            log.info("2.1.21 写入手机操作轨迹，接收参数：【{}, {}, {}, {}】", examId, candidateId, operatorType, takeTime);
            CandidateLogOther candidateLog = new CandidateLogOther();
            candidateLog.setId(UUIDUtils.randomUUID());
            candidateLog.setExamId(examId);
            candidateLog.setCandidateId(candidateId);
            candidateLog.setBeginTime(LocalDateTime.now());
            candidateLog.setEventType(LogEventType.ALERT_LOG.getCode());
            candidateLog.setOperateType(Integer.parseInt(operatorType));
            AppVo app = new AppVo();
            app.setClient("app");
            app.setTakeTime(takeTime);
            candidateLog.setOperateContent(JSON.toJSONString(app));
            iLogService.appLog(candidateLog);
            //iCandidateLogOtherService.save(candidateLog);
            return ApiResponse.builder()
                    .code(BizStatusCode.KS_SUCCESS_OK.getCode())
                    .build();
        } catch (Exception ex) {
            logger.error("写入手机操作轨迹信息失败:{}", ex);
            return ApiResponse.builder().code(BizStatusCode.SAVE_APP_LOG_FAIL.getCode()).build();
        }
    }
}